// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.


#include <algorithm>
#include <codecvt>
#include <thread>
#include <dlfcn.h>

#include <android/api-level.h>
#include <android/native_window_jni.h>
#include <core/hw/aes/key.h>
#include <core/loader/smdh.h>
#include <core/system_titles.h>

#include <core/hle/service/cfg/cfg.h>
#include <core/cheats/gateway_cheat.h>
#include <android/log.h>
#include "audio_core/dsp_interface.h"
#include "common/arch.h"

#if CITRA_ARCH(arm64)

#include "common/aarch64/cpu_detect.h"

#elif CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h"
#endif

#include "common/common_paths.h"
#include "common/dynamic_library/dynamic_library.h"
#include "common/file_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/frontend/applets/default_applets.h"
#include "core/frontend/camera/factory.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/nfc/nfc.h"
#include "core/loader/loader.h"
#include "core/savestate.h"
#include "jni/android_common/android_common.h"
#include "jni/applets/mii_selector.h"
#include "jni/applets/swkbd.h"
#include "jni/camera/ndk_camera.h"
#include "jni/camera/still_image_camera.h"
#include "jni/config.h"

#ifdef ENABLE_OPENGL

#include "jni/emu_window/emu_window_gl.h"

#endif
#ifdef ENABLE_VULKAN

#include "jni/emu_window/emu_window_vk.h"

#endif

#include "jni/game_settings.h"
#include "jni/id_cache.h"
#include "jni/input_manager.h"
#include "jni/ndk_motion.h"
#include "video_core/debug_utils/debug_utils.h"
#include "video_core/gpu.h"
#include "video_core/renderer_base.h"
#include "aiwu.h"
#include "network/network.h"
#include "mem_region.h"

jobjectArray ToJStringArray(const std::vector<std::string> &strs) {
    JNIEnv *env = IDCache::GetEnvForThread();
    jobjectArray array =
            env->NewObjectArray(strs.size(), env->FindClass("java/lang/String"),
                                env->NewStringUTF(""));
    for (int i = 0; i < strs.size(); ++i) {
        env->SetObjectArrayElement(array, i, ToJString(env, strs[i]));
    }
    return array;
}

void NetPlayGenerateConsoleId() {
    auto cfg = Service::CFG::GetModule(Core::System::GetInstance());
    auto [random_number, console_id] = cfg->GenerateConsoleUniqueId();
    cfg->SetConsoleUniqueId(random_number, console_id);
    cfg->UpdateConfigNANDSavegame();
}

void AddNetPlayMessage(jint type, jstring msg) {
    IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
                                                     IDCache::GetAddNetplayMessage25(), type, msg);
}

void AddNetPlayMessage(int type, const std::string &msg) {
    AddNetPlayMessage(type, ToJString(IDCache::GetEnvForThread(), msg));
}

bool NetworkInit() {
    bool result = Network::Init();

    if (!result) {
        return false;
    }

    if (auto member = Network::GetRoomMember().lock()) {
        // register the network structs to use in slots and signals
        member->BindOnStateChanged(
                [](const Network::RoomMember::State &state) {
                    NetPlayStatus status;
                    std::string msg;
                    switch (state) {
                        case Network::RoomMember::State::Uninitialized:
                            status = NetPlayStatus::ROOM_UNINITIALIZED;
                            break;
                        case Network::RoomMember::State::Idle:
                            status = NetPlayStatus::ROOM_IDLE;
                            break;
                        case Network::RoomMember::State::Joining:
                            status = NetPlayStatus::ROOM_JOINING;
                            break;
                        case Network::RoomMember::State::Joined:
                            status = NetPlayStatus::ROOM_JOINED;
                            break;
                        case Network::RoomMember::State::Moderator:
                            status = NetPlayStatus::ROOM_MODERATOR;
                            break;
                    }
                    AddNetPlayMessage(static_cast<int>(status), msg);
                });
        member->BindOnError(
                [](const Network::RoomMember::Error &error) {
                    NetPlayStatus status;
                    std::string msg;
                    switch (error) {
                        case Network::RoomMember::Error::LostConnection:
                            status = NetPlayStatus::LOST_CONNECTION;
                            break;
                        case Network::RoomMember::Error::HostKicked:
                            status = NetPlayStatus::HOST_KICKED;
                            break;
                        case Network::RoomMember::Error::UnknownError:
                            status = NetPlayStatus::UNKNOWN_ERROR;
                            break;
                        case Network::RoomMember::Error::NameCollision:
                            status = NetPlayStatus::NAME_COLLISION;
                            break;
                        case Network::RoomMember::Error::MacCollision:
                            status = NetPlayStatus::MAC_COLLISION;
                            break;
                        case Network::RoomMember::Error::ConsoleIdCollision:
                            status = NetPlayStatus::CONSOLE_ID_COLLISION;
                            NetPlayGenerateConsoleId();
                            break;
                        case Network::RoomMember::Error::WrongVersion:
                            status = NetPlayStatus::WRONG_VERSION;
                            break;
                        case Network::RoomMember::Error::WrongPassword:
                            status = NetPlayStatus::WRONG_PASSWORD;
                            break;
                        case Network::RoomMember::Error::CouldNotConnect:
                            status = NetPlayStatus::COULD_NOT_CONNECT;
                            break;
                        case Network::RoomMember::Error::RoomIsFull:
                            status = NetPlayStatus::ROOM_IS_FULL;
                            break;
                        case Network::RoomMember::Error::HostBanned:
                            status = NetPlayStatus::HOST_BANNED;
                            break;
                        case Network::RoomMember::Error::PermissionDenied:
                            status = NetPlayStatus::PERMISSION_DENIED;
                            break;
                        case Network::RoomMember::Error::NoSuchUser:
                            status = NetPlayStatus::NO_SUCH_USER;
                            break;
                    }
                    AddNetPlayMessage(static_cast<int>(status), msg);
                });
        member->BindOnStatusMessageReceived(
                [](const Network::StatusMessageEntry &status_message) {
                    NetPlayStatus status = NetPlayStatus::NO_ERROR;
                    std::string msg(status_message.nickname);
                    switch (status_message.type) {
                        case Network::IdMemberJoin:
                            status = NetPlayStatus::MEMBER_JOIN;
                            break;
                        case Network::IdMemberLeave:
                            status = NetPlayStatus::MEMBER_LEAVE;
                            break;
                        case Network::IdMemberKicked:
                            status = NetPlayStatus::MEMBER_KICKED;
                            break;
                        case Network::IdMemberBanned:
                            status = NetPlayStatus::MEMBER_BANNED;
                            break;
                        case Network::IdAddressUnbanned:
                            status = NetPlayStatus::ADDRESS_UNBANNED;
                            break;
                    }
                    AddNetPlayMessage(static_cast<int>(status), msg);
                });
        member->BindOnChatMessageRecieved(
                [](const Network::ChatEntry &chat) {
                    NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE;
                    std::string msg(chat.nickname);
                    msg += ": ";
                    msg += chat.message;
                    AddNetPlayMessage(static_cast<int>(status), msg);
                });
    }

    return true;
}

NetPlayStatus NetPlayCreateRoom(const std::string &ipaddress, int port, const std::string &password,
                                const std::string &username) {
    auto member = Network::GetRoomMember().lock();
    if (!member) {
        return NetPlayStatus::NETWORK_ERROR;
    }

    if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
        return NetPlayStatus::ALREADY_IN_ROOM;
    }

    auto room = Network::GetRoom().lock();
    if (!room) {
        return NetPlayStatus::NETWORK_ERROR;
    }

    if (!room->Create(ipaddress, "", "", port, password,
                      8, username, "", 0, std::make_unique<Network::VerifyUser::NullBackend>())) {
        return NetPlayStatus::CREATE_ROOM_ERROR;
    }

    std::string console = Service::CFG::GetConsoleIdHash(Core::System::GetInstance());
    member->Join(username, console, "127.0.0.1", port, 0, Network::NoPreferredMac, password);
    return NetPlayStatus::NO_ERROR;
}

NetPlayStatus NetPlayJoinRoom(const std::string &ipaddress, int port, const std::string &password,
                              const std::string &username) {
    auto member = Network::GetRoomMember().lock();
    if (!member) {
        return NetPlayStatus::NETWORK_ERROR;
    }

    if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
        return NetPlayStatus::ALREADY_IN_ROOM;
    }

    std::string console = Service::CFG::GetConsoleIdHash(Core::System::GetInstance());
    member->Join(username, console, ipaddress.c_str(), port, 0, Network::NoPreferredMac, password);
    return NetPlayStatus::NO_ERROR;
}

void NetPlaySendMessage(const std::string &msg) {
    if (auto room = Network::GetRoomMember().lock()) {
        if (room->GetState() != Network::RoomMember::State::Joined &&
            room->GetState() != Network::RoomMember::State::Moderator) {

            return;
        }
        room->SendChatMessage(msg);
    }
}

void NetPlayKickUser(const std::string &username) {
    if (auto room = Network::GetRoomMember().lock()) {
        auto members = room->GetMemberInformation();
        auto it = std::find_if(members.begin(), members.end(),
                               [&username](const Network::RoomMember::MemberInformation &member) {
                                   return member.nickname == username;
                               });
        if (it != members.end()) {
            room->SendModerationRequest(Network::RoomMessageTypes::IdModKick, username);
        }
    }
}

std::vector<std::string> NetPlayRoomInfo() {
    std::vector<std::string> infolist;
    if (auto room = Network::GetRoomMember().lock()) {
        auto members = room->GetMemberInformation();
        if (!members.empty()) {
            // name
            infolist.push_back(room->GetRoomInformation().name);
            // port
            infolist.push_back(std::to_string(room->GetRoomInformation().port));
            // all
            for (const auto &member: members) {
                infolist.push_back(member.nickname);
            }
        }
    }
    return infolist;
}

bool NetPlayIsJoined() {
    auto member = Network::GetRoomMember().lock();
    if (!member) {
        return false;
    }

    if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
        return true;
    }

    return false;
}

bool NetPlayIsHostedRoom() {
    if (auto room = Network::GetRoom().lock()) {
        return room->GetState() == Network::Room::State::Open;
    }
    return false;
}

void NetPlayLeaveRoom() {
    if (auto room = Network::GetRoom().lock()) {
        // if you are in a room, leave it
        if (auto member = Network::GetRoomMember().lock()) {
            member->Leave();
        }

        // if you are hosting a room, also stop hosting
        if (room->GetState() == Network::Room::State::Open) {
            room->Destroy();
        }
    }
}

std::string NetPlayGetConsoleId() {
    auto cfg = Service::CFG::GetModule(Core::System::GetInstance());
    u64 console_id = cfg->GetConsoleUniqueId();
    return fmt::format("{:016X}", console_id);
}

void NetworkShutdown() {
    Network::Shutdown();
}

extern "C" {
static Cheats::CheatEngine &GetCheatEngine() {
    Core::System &system{Core::System::GetInstance()};
    return system.CheatEngine();
}
void Java_org_citra_citra_1emu_NativeLibrary_updateCheat25(JNIEnv *env, jobject clazz,
                                                           jobjectArray stringArray) {
    if (stringArray == nullptr || env->GetArrayLength(stringArray) == 0) {
        GetCheatEngine().ClearCheats25();
        return;
    }
    int stringCount = env->GetArrayLength(stringArray);
    GetCheatEngine().ClearCheats25();
    for (int i = 0; i < stringCount; i++) {
        auto string = (jstring) (env->GetObjectArrayElement(stringArray, i));
        if (string == nullptr) {
            continue;
        }
        const std::string code = GetJString(env, string);
        auto cheat = std::make_shared<Cheats::GatewayCheat>("", code, "");
        cheat->SetEnabled(true);
        GetCheatEngine().AddCheat(cheat);
    }
}

void Java_org_citra_citra_1emu_NativeLibrary_switchBottomScreen25(JNIEnv *env, jobject clazz,
                                                                  jboolean jEnable) {
    auto &system = Core::System::GetInstance();
    if (system.IsPoweredOn()) {
        system.GPU().Renderer().SwitchBottomScreen25(jEnable);
    }
}

void Java_org_citra_citra_1emu_NativeLibrary_resetGame25(JNIEnv *env, jobject clazz) {
    auto &system = Core::System::GetInstance();
    if (system.IsPoweredOn()) {
        system.RequestReset();
    }
}

jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent25(JNIEnv *env, jobject obj,
                                                                      jstring j_device, jint axis,
                                                                      jfloat value, jboolean is_x) {
    if (is_x) {
        value = std::clamp(value, -1.f, 1.f);
    } else {
        value = std::clamp(-value, -1.f, 1.f);
    }
    return static_cast<jboolean>(InputManager::AnalogHandler()->MoveJoystick25(axis, value, is_x));
}
jint Java_org_citra_citra_1emu_NativeLibrary_netPlayCreateRoom25(JNIEnv *env, jobject clazz,
                                                                 jstring ipaddress, jint port,
                                                                 jstring password,
                                                                 jstring username) {
    return static_cast<jint>(NetPlayCreateRoom(GetJString(env, ipaddress), port,
                                               GetJString(env, password),
                                               GetJString(env, username)));
}

jint Java_org_citra_citra_1emu_NativeLibrary_netPlayJoinRoom25(JNIEnv *env, jobject clazz,
                                                               jstring ipaddress, jint port,
                                                               jstring password, jstring username) {
    return static_cast<jint>(NetPlayJoinRoom(GetJString(env, ipaddress), port,
                                             GetJString(env, password),
                                             GetJString(env, username)));
}

jobjectArray Java_org_citra_citra_1emu_NativeLibrary_netPlayRoomInfo25(JNIEnv *env, jobject clazz) {
    return ToJStringArray(NetPlayRoomInfo());
}

jboolean Java_org_citra_citra_1emu_NativeLibrary_netPlayIsJoined25(JNIEnv *env, jobject clazz) {
    return NetPlayIsJoined();
}

jboolean Java_org_citra_citra_1emu_NativeLibrary_netPlayIsHostedRoom25(JNIEnv *env, jobject clazz) {
    return NetPlayIsHostedRoom();
}

void Java_org_citra_citra_1emu_NativeLibrary_netPlaySendMessage25(JNIEnv *env, jobject clazz,
                                                                  jstring msg) {
    NetPlaySendMessage(GetJString(env, msg));
}

void Java_org_citra_citra_1emu_NativeLibrary_netPlayKickUser25(JNIEnv *env, jobject clazz,
                                                               jstring username) {
    NetPlayKickUser(GetJString(env, username));
}

void Java_org_citra_citra_1emu_NativeLibrary_netPlayLeaveRoom25(JNIEnv *env, jobject clazz) {
    NetPlayLeaveRoom();
}

jstring Java_org_citra_citra_1emu_NativeLibrary_netPlayGetConsoleId25(JNIEnv *env, jobject clazz) {
    return ToJString(env, NetPlayGetConsoleId());
}


jintArray Java_org_citra_citra_1emu_NativeLibrary_loadPageTable(JNIEnv *env, jobject obj) {
    u32 start_addr = 0;
    u32 mem_size = 0;
    std::vector<u32> pages;
    Core::System &system{Core::System::GetInstance()};
    auto pagetable = system.Memory().GetCurrentPageTable();

    for (u32 i = 0; i < pagetable->GetPointerArray().size(); ++i) {
        auto p = pagetable->pointers[i];
        if (p != nullptr) {
            u32 addr = i << Memory::CITRA_PAGE_BITS;
            if ((start_addr + mem_size) != addr) {
                if (mem_size > 0) {
                    pages.push_back(start_addr);
                    pages.push_back(mem_size);
                    mem_size = 0;
                }
                start_addr = addr;
            }
            mem_size += Memory::CITRA_PAGE_SIZE;
        }
    }

    jintArray array = env->NewIntArray(pages.size());
    env->SetIntArrayRegion(array, 0, pages.size(), reinterpret_cast<const jint *>(pages.data()));
    return array;
}

jbyteArray Java_org_citra_citra_1emu_NativeLibrary_loadPage(JNIEnv *env, jobject obj, jint index) {
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->GetPointerArray()[index];
    if (p != nullptr) {
        jbyteArray array = env->NewByteArray(Memory::CITRA_PAGE_SIZE);
        env->SetByteArrayRegion(array, 0, Memory::CITRA_PAGE_SIZE,
                                reinterpret_cast<const jbyte *>(p));
        return array;
    } else {
        return nullptr;
    }
}

jint Java_org_citra_citra_1emu_NativeLibrary_readMemory(JNIEnv *env, jobject obj, jint jAddr,
                                                        jint valueType) {
    u32 addr = static_cast<u32>(jAddr);
    u32 index = addr >> Memory::CITRA_PAGE_BITS;
    u32 offset = addr & Memory::CITRA_PAGE_MASK;
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->pointers[index];
    if (p != nullptr) {
        if (valueType == 0) {
            return *reinterpret_cast<u32 *>(p + offset);
        } else if (valueType == 1) {
            return *reinterpret_cast<u16 *>(p + offset);
        } else if (valueType == 2) {
            return *reinterpret_cast<u8 *>(p + offset);
        }
    }
    return 0;
}

void Java_org_citra_citra_1emu_NativeLibrary_writeMemory(JNIEnv *env, jobject obj, jint jAddr,
                                                         jint valueType, jint value) {
    u32 addr = static_cast<u32>(jAddr);
    u32 index = addr >> Memory::CITRA_PAGE_BITS;
    u32 offset = addr & Memory::CITRA_PAGE_MASK;
    Core::System &system{Core::System::GetInstance()};
    auto p = system.Memory().GetCurrentPageTable()->pointers[index];
    if (p != nullptr) {
        if (valueType == 0) {
            *reinterpret_cast<u32 *>(p + offset) = static_cast<u32>(value);
        } else if (valueType == 1) {
            *reinterpret_cast<u16 *>(p + offset) = static_cast<u16>(value);
        } else if (valueType == 2) {
            *reinterpret_cast<u8 *>(p + offset) = static_cast<u8>(value);
        }
    }
}
jintArray Java_org_citra_citra_1emu_NativeLibrary_searchMemory(JNIEnv *env, jobject obj,
                                                               jint start_addr,
                                                               jint stop_addr,
                                                               jint value_type,
                                                               jint search_type,
                                                               jint scan_type,
                                                               jint value) {
    return searchMemoryRegion(start_addr, stop_addr, value_type, search_type, scan_type, value);
}
jintArray Java_org_citra_citra_1emu_NativeLibrary_getSearchResults(JNIEnv *env, jobject obj) {
    return getSearchResults();
}

void Java_org_citra_citra_1emu_NativeLibrary_resetSearchResults(JNIEnv *env, jobject obj) {
    resetSearchResults();
}
}
